package org.naivecode.audio;

import java.util.ArrayList;
import java.util.List;

public class LinearSequence {
	private final List<Extremum> dataPoints = new ArrayList<Extremum>();
	private final float start;

	private final ApproximationContext center;
	private final ApproximationContext magnitude;
	private final ApproximationContext leftRidge;
	private final ApproximationContext rightRidge;

	private boolean recalculateLine = true;

	public LinearSequence(float startOffset) {
		this.start = startOffset;
		this.center = new CenterApproximationContext();
		this.magnitude = new MagnitudeApproximationContext();
		this.leftRidge = new LeftRidgeApproximationContext();
		this.rightRidge = new RightRidgeApproximationContext();
	}

	/**
	 * @return True if the given point fits this sequence. false otherwise.
	 */
	public boolean fits(Extremum e) {
		if (dataPoints.size() > 0) {
			float x = start + dataPoints.size();
			if (e.frequencySpread > 1) {
				final float leftRidge = getLeftRidgeLine().extrapolate(x);
				final float rightRidge = getLeftRidgeLine().extrapolate(x);

				final float el = e.frequency - e.frequencySpread;
				
				final float ll = el - leftRidge;
				final float rl = e.frequency - leftRidge;
				final float lr = el - rightRidge;
				final float rr = e.frequency - rightRidge;
				if (( ll < 0 && rl > 0 ) || (ll < 0 && rr < 0) || rr > 0 && lr < 0) {
					return true;
				}
			}
		}
		return false;
	}

	public void append(Extremum e) {
		dataPoints.add(e);
		recalculateLine = true;
	}

	public Line getCenterLine() {
		if (recalculateLine) {
			fitLines();
		}
		return center;
	}

	public Line getMagnitudeLine() {
		if (recalculateLine) {
			fitLines();
		}
		return magnitude;
	}

	public Line getLeftRidgeLine() {
		if (recalculateLine) {
			fitLines();
		}
		return leftRidge;
	}

	public Line getRightRigdeLine() {
		if (recalculateLine) {
			fitLines();
		}
		return rightRidge;
	}

	/**
	 * Weighted least squares approximation
	 */
	private void fitLine(ApproximationContext cntx) {
		float sw = 0;
		float sxw = 0;
		float syw = 0;
		float sxxw = 0;
		float sxyw = 0;

		float x = start;
		for (Extremum e : dataPoints) {
			final float w = cntx.getWeight(e);
			final float y = cntx.getValue(e);
			sw += w;
			sxw += w * x;
			syw += w * y;
			sxxw += x * x * w;
			sxyw += x * y * w;
			//
			x++;
		}
		final float shift = (syw * sxxw - sxw * sxyw) / (sw * sxxw - sxw * sxw);
		final float slope = (sxyw - shift * sxw) / sxxw;
		cntx.setShift(shift);
		cntx.setSlope(slope);

	}

	private void fitLines() {
		fitLine(center);
		fitLine(magnitude);
		fitLine(leftRidge);
		fitLine(rightRidge);
		recalculateLine = false;
	}

	public float getStart() {
		return start;
	}

	public float getEnd() {
		return start + dataPoints.size();
	}

	public interface Line {
		float getSlope();

		float getShift();

		float extrapolate(float x);
	}

	private static abstract class ApproximationContext implements Line {
		private float slope;
		private float shift;

		abstract public float getValue(Extremum e);

		abstract public float getWeight(Extremum e);

		public float getSlope() {
			return slope;
		}

		public void setSlope(float slope) {
			this.slope = slope;
		}

		public float getShift() {
			return shift;
		}

		public void setShift(float shift) {
			this.shift = shift;
		}

		@Override
		public float extrapolate(float x) {
			return x * slope + shift;
		}
	}

	private static class CenterApproximationContext extends
			ApproximationContext {

		@Override
		public float getValue(Extremum e) {
			return e.frequency - e.frequencySpread / 2;
		}

		@Override
		public float getWeight(Extremum e) {
			return 1 - e.frequencySpread / 30;
		}

	}

	private static class MagnitudeApproximationContext extends
			ApproximationContext {
		@Override
		public float getValue(Extremum e) {
			return e.magnitude;
		}

		@Override
		public float getWeight(Extremum e) {
			return 1;
		}
	}

	private static class LeftRidgeApproximationContext extends
			ApproximationContext {

		@Override
		public float getValue(Extremum e) {
			return e.frequency - e.frequencySpread;
		}

		@Override
		public float getWeight(Extremum e) {
			return 1;
		}

	}

	private static class RightRidgeApproximationContext extends
			ApproximationContext {

		@Override
		public float getValue(Extremum e) {
			return e.frequency;
		}

		@Override
		public float getWeight(Extremum e) {
			return 1;
		}
	}
}
